"""
Artificitial Neural Network
"""
import csv,copy,math
from itertools import *
import operator

from Tkinter import *
from ScrolledText import *

def to_color_string(red,green=None,blue=None):
    # red, green, blue parameterized [0..1]
    if green is None:
        if type(red) in (type(()),type([])):
            red,green,blue = red
        else:
            green=blue=red
    if type(red) is type(''):
        # named color
        return red
    red*=4095;green*=4095;blue*=4095;
    red=int(red);green=int(green);blue=int(blue);
    color = '#'
    #print red,green,blue
    color += (hex(red)[2:].zfill(3)+
              hex(green)[2:].zfill(3)+
              hex(blue)[2:].zfill(3))
    return color

class GuiObject:
    def __init__(self):
        self.guiInfo = {}
    def getCanvas(self):
        canvas = self.guiInfo.get('canvas',None)
        return canvas
    def getColor(self):
        return self.guiInfo['color']
    def resetGui(self):
        pass
    def getId(self):
        return self.guiInfo['id']
    def getTag(self):
        return self.guiInfo['tag']
    

class Connection(GuiObject):
    def __init__(self,start,end):
        GuiObject.__init__(self)
        self.neurons = [start,end]
        self.input,self.weight = 0,0
        self.isEnabled = True
        self.guiInfo = {'color':{True:[0,0,0],
                                 False:[.8,.8,.8]},
                        'start':[0,0],
                        'end':[0,0],
                        }
    def __del__(self):
        print 'Deleting connection',self
        self.removeGui()
    def getNeurons(self):
        start,end = self.neurons
        return start,end
    def checkNeurons(self,start,end):
        if [start,end] == self.neurons:
            return True
        return False
    def setWeight(self,weight):
        self.weight = weight
    def setInput(self,input):
        self.input = input
    def getInput(self):
        return self.input
    def test(self):
        if self.isEnabled:
            return self.input*self.weight
    def disable(self):
        self.isEnabled = False
        self.resetGui()
    def enable(self):
        self.isEnabled = True
        self.resetGui()

    index = 0
    def removeGui(self):
        canvas = self.getCanvas()
        if canvas:
            tag = self.getTag()
            canvas.delete(tag)
    def initGui(self,canvas):
        # setStart and setEnd should already have been called
        self.guiInfo['canvas'] = canvas
        x0,y0,x1,y1 = self.getLine()
        tag = 'connection%s'%Connection.index
        Connection.index += 1
        id = canvas.create_line(
            x0,y0,x1,y1,
            arrow='last',
            fill=self.getColor(),
            tag=tag,
            )
        self.guiInfo['tag'] = tag
        self.guiInfo['id'] = id
        return tag
    def resetGui(self):
        canvas = self.getCanvas()
        if canvas:
            id = self.getId()
            color = self.getColor()
            line = self.getLine()
            canvas.itemconfig(id,fill=color)
            canvas.coords(id,*line)
    def getLine(self):
        start,end = self.getNeurons()
        orad = start.getOuterRadius()
        x0,y0 = self.getStart()
        x1,y1 = self.getEnd()
        dx,dy = x1-x0,y1-y0
        ds = math.hypot(dx,dy)

        t = orad/ds
        u = (t*dx,t*dy)
        r0 = (x0,y0)
        r1 = (r0[0]+u[0],r0[1]+u[1])

        t = (ds-2*orad)/ds
        u = (t*dx,t*dy)
        r2 = (r1[0]+u[0],r1[1]+u[1])
        #dx -= start.getOuterRadius()+end.getOuterRadius()
        #dy -= start.getOuterRadius()+end.getOuterRadius()
        #return x0+orad,y0-orad/2.,x0+dx-orad,y0+dy
        return r1[0],r1[1],r2[0],r2[1]
    def getStart(self):
        start,end = self.getNeurons()
        return start.getPosition()
    def getEnd(self):
        start,end = self.getNeurons()
        return end.getPosition()
    def getColor(self):
        color = self.guiInfo['color'][self.isEnabled]
        return to_color_string(color)

        
class Neuron(GuiObject):
    typeCycle = [None,'io','and','or','not',]
    typeText = {None:['?','black'],
                'and':['And','black'],
                'or':['Or','white'],
                'not':['Not','white'],
                'io':['I/O','grey'],
                }
    def __init__(self):
        GuiObject.__init__(self)
        self.type = None
        self.value = 0
        self.thresh = 0
        # connections
        self.outbound,self.inbound = [],[]
        
        self.guiInfo = {'color':{None:'grey',
                                 'and':'yellow',
                                 'or':'blue',
                                 'not':'brown',
                                 'io':['black','white'],
                                 },
                        'pos':[0,0],
                        'radius': 20,
                        'outerRadius':5,
                        'outerColor':'white',
                        }
    def cycleType(self,includeNone=False):
        index = self.typeCycle.index(self.type)+1
        if index == len(self.typeCycle):
            index = 0 if includeNone else 1
        type = self.typeCycle[index]
        self.setType(type)
    def getType(self):
        return self.type
    def setType(self,type):
        self.type = type
        self.setup()
        self.resetGui()
    def cycleValue(self):
        self.setValue(abs(self.value-1))
        self.resetGui()
    def setValue(self,value):
        self.value = value
        self.setup()
        self.resetGui()
    def setup(self):
        if self.type == 'and':
            weightSum = 0
            for con in self.inbound:
                con.setWeight(1)
                weightSum += 1
            self.thresh = weightSum
        elif self.type == 'or':
            for con in self.inbound:
                con.setWeight(1)
            self.thresh = 1
        elif self.type == 'not':
            for con in self.inbound:
                con.setWeight(-1)
            self.thresh = 0
    def addInbound(self,connection):
        self.inbound.append(connection)
        self.setup()
    def addOutbound(self,connection):
        self.outbound.append(connection)
        self.setup()
    def removeConnection(self,connection):
        if connection in self.inbound:
            self.inbound.remove(connection)
        if connection in self.outbound:
            self.outbound.remove(connection)
        self.setup()
    def sigmoid(self,a):
        value = 1/(1+math.exp(-1*(a-self.thresh)))
        return value
    doSigmoid = False
    def test(self):
        if self.getType() != 'io':
            # and, or and not neurons
            inboundSum = 0
            for con in self.inbound:
                inboundSum += con.test()
            if not Neuron.doSigmoid:
                output = 0
                self.setOutline('black')
                if inboundSum >= self.thresh:
                    self.setOutline('white')
                    output = 1
                self.setUnderText(output)
            else:
                output = self.sigmoid(inboundSum)
                ocolval = output
                #print output
                if 0:
                    if output < 0.5:
                        ocolval = (output/0.5)*0.2
                    else:
                        ocolval = ((output-0.5)/0.5)*0.2 + 0.8
                args = (inboundSum,self.thresh,output)
                text = 'a:%s\nThreshold: %s\nSigmoid(a): %s'%args
                self.setUnderText(text)
                self.setOutline([ocolval,]*3)
        else:
            # I/O neurons
            output = self.value

        for con in self.outbound:
            con.setInput(output)

        self.resetGui()

        return output

    index = 0
    def initGui(self,canvas,x,y):
        self.guiInfo['canvas'] = canvas
        self.guiInfo['pos'] = [x,y]
        bbox = self.getBbox()
        tag = 'neuron%s'%Neuron.index
        Neuron.index += 1
        id = canvas.create_oval(bbox[0],bbox[1],bbox[2],bbox[3],
                                fill=self.getColor(),
                                tag=tag,
                                )
        obbox = self.getOuterBbox()
        ocolor = self.getOuterColor()
        outlineId = canvas.create_oval(obbox[0],obbox[1],obbox[2],obbox[3],
                                       fill=ocolor,
                                       tag=tag,
                                       )
        self.guiInfo['outlineId']=outlineId
        canvas.tag_lower(outlineId,id)

        text,color = self.typeText[self.type]
        textId = canvas.create_text(x,y,
                                    text=text,
                                    fill=color,
                                    tag=tag,
                                    )

        orad = self.getOuterRadius()
        underTextId = canvas.create_text(x,y+orad,
                                         tag = tag,
                                         anchor='n',
                                         justify='center',
                                         )
        
        self.guiInfo['tag'] = tag
        self.guiInfo['id'] = id
        self.guiInfo['textId'] = textId
        self.guiInfo['underTextId'] = underTextId
        return tag
    def resetGui(self):
        #print 'resetting neuron gui'
        canvas = self.getCanvas()
        if canvas:
            x,y = self.getPosition()

            # inner circle
            id = self.getId()
            bbox = self.getBbox()
            color = self.getColor()
            canvas.itemconfigure(id,
                                 fill=color,
                                 )
            canvas.coords(id,*bbox)

            # inner text
            textId = self.getTextId()
            text,textColor = self.typeText[self.type]
            canvas.itemconfigure(textId,
                                 text=text,
                                 fill=textColor,
                                 )
            canvas.coords(textId,x,y)

            utextId = self.guiInfo['underTextId']
            orad = self.getOuterRadius()
            canvas.coords(utextId,x,y+orad+10)

            # outer circle
            outlineId = self.guiInfo['outlineId']
            obbox = self.getOuterBbox()
            ocolor = self.getOuterColor()
            canvas.itemconfigure(outlineId,
                                 fill=ocolor,
                                 )
            canvas.coords(outlineId,
                          *obbox)


    def setUnderText(self,text):
        canvas = self.getCanvas()
        if canvas:
            id = self.guiInfo['underTextId']
            canvas.itemconfig(id,text=text)
    def setOutline(self,color=None):
        if color is None:
            self.guiInfo['outerColor'] = 'white'
        else:
            self.guiInfo['outerColor'] = to_color_string(color)
        self.resetGui()
    def getTextId(self):
        return self.guiInfo['textId']
    def setPosition(self,x,y):
        self.guiInfo['pos'] = [x,y]
        for con in self.outbound:
            con.resetGui()
        for con in self.inbound:
            con.resetGui()
        self.resetGui()
    def getPosition(self):
        return tuple(self.guiInfo['pos'])
    def setRadius(self,radius):
        self.guiInfo['radius'] = radius
        self.resetGui()
    def getRadius(self):
        return self.guiInfo['radius']
    def setOuterRadius(self,radius):
        self.guiInfo['outerRadius'] = radius
        self.resetGui()
    def getOuterRadius(self):
        radius = self.getRadius()
        outerRadius = radius + self.guiInfo['outerRadius']
        return outerRadius
    def getBbox(self):
        x,y = self.getPosition()
        r = self.getRadius()
        bbox = [x-r,y-r,x+r,y+r]
        return bbox
    def getOuterBbox(self):
        x,y = self.getPosition()
        r = self.getOuterRadius()
        bbox = [x-r,y-r,x+r,y+r]
        return bbox
    def getColor(self):
        color = self.guiInfo['color'][self.type]
        if type(color) is type([]):
            color = color[self.value]
        return to_color_string(color)
    def getOuterColor(self):
        color = self.guiInfo['outerColor']
        if type(color) is type([]):
            color = color[self.value]
        return to_color_string(color)


HELP = """
McCulloch and Pitts neuron model

How to use:

- Left-click the white field to add neurons. 
--- New neurons have no type
--- Left-click and drag to move a neuron
- Right-click on neurons to change their type.
- Control-left-click to pick the first neuron of a connected pair
--- Select the second by letting go of control and left-clicking it
- Right-click a connection to delete it.
- Left-click I/O neurons to set their value (green:true, red:false)
- Press space (or click button) to run network
- Click sigmoid checkbox to have neurons be evaluated using sigmoid function:
     Sigmoid(a) = 1/(1+exp(-1*(a-threshold)))

--------------------------
Neuron types
--------------------------
- Input/Output: Neurons whose value is user defined
   left-click I/O neurons to change their value

- And: If all inbound connections are true, then neuron forwards true

- Or: If at least one inbound connections is true, then neuron forwards true

- Not: Input from first inbound connection is reversed and forwarded

"""

class NetworkApp(Frame):
    connectionStartNeuron = None
    currentState = 'insert'
    stateCycle = ['insert','connect']
    movingNeuron,neuronToBeMoved = False,False
    
    def __init__(self,parent,**kw):
        Frame.__init__(self,parent,**kw)
        #self.tk_focusFollowsMouse()

        parent.bind('<Escape>',self.event_escape)
        parent.bind('<space>',lambda e,s=self:s.runNetwork())

        self.neurons = {}
        self.connections = {}

        canvasTextFrame = Frame(self)
        self.canvas = Canvas(canvasTextFrame,bd=2,relief='sunken',bg='white')

        self.canvas.bind('<ButtonPress-1>',self.event_buttonPress1)
        self.canvas.bind('<ButtonRelease-1>',self.event_buttonRelease1)
        self.canvas.bind('<Control-ButtonRelease-1>',
                         self.event_controlButtonRelease1)
        self.canvas.bind('<B1-Motion>',self.event_motion1)
        self.canvas.bind('<ButtonPress-3>',self.event_buttonPress3)
        self.canvas.bind('<ButtonRelease-3>',self.event_buttonRelease3)
        self.canvas.bind('<B3-Motion>',self.event_motion3)
        self.canvas.bind('<Motion>',self.event_motion)

        
        self.text = ScrolledText(canvasTextFrame,
                                 wrap='word',
                                 width=40,
                                 bg='#eee',
                                 state='disabled',
                                 bd=2,relief='sunken',
                                 )
        self.setText(HELP)

        self.button = Button(self,text='Press Space to Run',
                             command=self.runNetwork)
        self.sigVar = IntVar()
        self.sigVar.set(False)
        sigmoidCheck = Checkbutton(self,
                                   command=self.setSigmoid,
                                   variable=self.sigVar,
                                   text='Sigmoid')
        

        #self.canvas.focus_set()

        self.canvas.pack(side='left',expand=1,fill='both')
        self.text.pack(side='right',expand=1,fill='y')
        canvasTextFrame.pack(side='top',expand=1,fill='both')
        self.button.pack(side='bottom')

        sigmoidCheck.pack(side='bottom')
        
        self.pack(expand=1,fill='both')

    def setText(self,text):
        self.text['state']='normal'
        self.text.insert('end',text)
        self.text['state']='disabled'

    def setSigmoid(self):
        Neuron.doSigmoid = self.sigVar.get()
    def runNetwork(self):
        neuronsTested = []
        runNeurons = []
        for n in self.neurons.values():
            if (n.getType() == 'io'):
                runNeurons.append(n)
        while True:
            runCons = []
            for n in runNeurons:
                if 0:
                    if n in neuronsTested:
                        print 'ACK! Cycle!! Killing run...'
                        print n,n.getType()
                        break
                n.test()
                neuronsTested.append(n)
                for nn in n.outbound:
                    if nn not in runCons:
                        runCons.append(nn)
            if not runCons:
                break
            runNeurons = []
            for con in runCons:
                start,end = con.getNeurons()
                runNeurons.append(end)
        
    def findTags(self,x,y):
        ids = self.canvas.find_overlapping(x-1,y-1,x+1,y+1)
        tags = []
        for id in ids:
            ttags = self.canvas.gettags(id)
            for t in ttags:
                if t not in tags:
                    tags.append(t)
        return tags

    def print_event(self,event):
        print 
        for name,object in event.__dict__.items():
            print name,object

    def getState(self):
        return self.currentState
    def setState(self,state):
        self.currentState = state
    
    def event_escape(self,event):
        self.stopConnect()
        
    def event_buttonPress1(self,event):
        x,y = event.x,event.y
        tags = self.findTags(x,y)
        if self.getState() == 'insert':
            if tags:
                tag = tags[0]
                if tag in self.neurons:
                    neuron = self.neurons[tag]
                    self.neuronToBeMoved = neuron
                    #print 'neuron to be moved',neuron
                elif tag in self.connections:
                    pass

    def event_controlButtonRelease1(self,event):
        x,y = event.x,event.y
        tags = self.findTags(x,y)
        if tags:
            tag = tags[0]
            if tag in self.neurons:
                neuron = self.neurons[tag]
                self.currentState = 'connect'
                csn = self.connectionStartNeuron
                if csn:
                    csn.setOutline()
                self.connectionStartNeuron = neuron
                neuron.setOutline('magenta')

    def event_buttonRelease1(self,event):
        x,y = event.x,event.y
        tags = self.findTags(x,y)
        if self.getState() == 'insert':
            if not self.movingNeuron:
                if tags:
                    tag = tags[0]
                    if tag in self.neurons:
                        neuron = self.neurons[tag]
                        neuron.cycleValue()
                else:
                    neuron = Neuron()
                    tag = neuron.initGui(self.canvas,x,y)
                    self.neurons[tag] = neuron
            self.neuronToBeMoved,self.movingNeuron = False,False
        elif self.getState() == 'connect':
            if tags:
                tag = tags[0]
                if tag in self.neurons:
                    end = self.neurons[tag]
                    start = self.connectionStartNeuron
                    # check all the connections to see if this has
                    # already been added
                    if start == end:
                        print 'Cannot connect neuron to itself, NOT adding...'
                        self.stopConnect()
                        return
                    for con in self.connections.values():
                        if (con.checkNeurons(start,end) or
                            con.checkNeurons(end,start) ):
                            # don't add the connection
                            print 'Duplicate connection, NOT adding...'
                            self.stopConnect()
                            return
                    con = Connection(start,end)
                    start.addOutbound(con)
                    end.addInbound(con)
                    tag = con.initGui(self.canvas)
                    self.connections[tag] = con
            self.stopConnect()
    def stopConnect(self):
        csn = self.connectionStartNeuron
        self.connectionStartNeuron = None
        self.setState('insert')
        for n in self.neurons.values():
            n.setOutline()
    def event_motion1(self,event):
        x,y = event.x,event.y
        if self.getState() == 'insert':
            if self.neuronToBeMoved:
                self.movingNeuron = True
                neuron = self.neuronToBeMoved
                #print 'neuron being moved',neuron
                neuron.setPosition(x,y)
            
        #print 'Button 1',x,y
    def event_buttonPress3(self,event):
        x,y = event.x,event.y
        if self.getState() == 'insert':
            pass
    def event_buttonRelease3(self,event):
        x,y = event.x,event.y
        tags = self.findTags(x,y)
        if tags:
            tag = tags[0]
            if tag in self.neurons:
                neuron = self.neurons[tag]
                neuron.cycleType()
            elif tag in self.connections:
                # delete the connection
                con = self.connections[tag]
                for n in self.neurons.values():
                    n.removeConnection(con)
                del self.connections[tag]
    def event_motion3(self,event):
        x,y = event.x,event.y
        if self.getState() == 'insert':
            print 'Button 3',x,y
    def event_motion(self,event):
        x,y = event.x,event.y
        if self.getState() == 'insert':
            pass


if __name__=='__main__':
    root = Tk()
    netApp = NetworkApp(root)
    root.mainloop()
